# 2.渲染进程中的 Event Loop(事件循环机制)

选自: 带你彻底弄懂 Event Loop (opens new window)

什么是 Event Loop? (opens new window)

# 宏队列和微队列

# 为什么消息队列要有宏队列和微队列之分

  1. 宏任务可以满足我们大部分的日常需求,但是宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,

# 宏队列,macrotask,也叫 tasks。

一些异步任务的回调会依次进入 macro task queue,等待后续被调用,这些异步任务包括:

  1. setTimeout
  2. setInterval
  3. setImmediate (Node 独有)
  4. requestAnimationFrame (浏览器独有)
  5. I/O
  6. UI rendering (浏览器独有)

# 微队列,microtask,也叫 jobs。

另一些异步任务的回调会依次进入 micro task queue,等待后续被调用,这些异步任务包括:

  1. process.nextTick (Node 独有)
  2. Promise
  3. Object.observe
  4. MutationObserver(用来监视 DOM 变动。比如节点的增减、属性的变动、文本内容的变动。) (注:这里只针对浏览器和 NodeJS)

# 浏览器的 Event Loop

我们先来看一张图,再看完这篇文章后,请返回来再仔细看一下这张图,相信你会有更深的理解。

在这里插入图片描述

这张图将浏览器的 Event Loop 完整的描述了出来,我来讲执行一个 JavaScript 代码的具体流程:

  1. 执行全局 Script 同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如 setTimeout 等);
  2. 全局 Script 代码执行完毕后,调用栈 Stack 会清空;
  3. 从微队列 microtask queue 中取出位于队首的回调任务,放入调用栈 Stack 中执行,执行完后 microtask queue 长度减 1;
  4. 继续取出位于队首的任务,放入调用栈 Stack 中执行,以此类推,直到直到把 microtask queue 中的所有任务都执行完毕。
  5. 注意,如果在执行 microtask 的过程中,又产生了 microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
  6. microtask queue 中的所有任务都执行完毕,此时 microtask queue 为空队列,调用栈 Stack 也为空;
  7. 取出宏队列 macrotask queue 中位于队首的任务,放入 Stack 中执行; 执行完毕后,调用栈 Stack 为空;
  8. 重复第 3-7 个步骤;
  9. 重复第 3-7 个步骤; ...... 可以看到,这就是浏览器的事件循环 Event Loop

这里归纳 3 个重点:

  1. 宏队列 macrotask 一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  2. 微任务队列中所有的任务都会被依次取出来执行,直到 microtask queue 为空;
  3. 图中没有画 UI rendering 的节点,因为这个是由浏览器自行判断决定的,但是只要执行 UI rendering,它的节点是在执行完所有的 microtask 之后,下一个 macrotask 之前,紧跟着执行 UI render。
    1. 所以导致一个问题:微任务,如果添加速度大于执行速度,并且一直添加,则微队列一直不为空,它就会阻塞 UI 线程
  • 好了,概念性的东西就这么多,来看几个示例代码,测试一下你是否掌握了:
console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
});

new Promise((resolve, reject) => {
  console.log(4);
  resolve(5);
}).then((data) => {
  console.log(data);
});

setTimeout(() => {
  console.log(6);
});

console.log(7);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 正确答案
1;
4;
7;
5;
2;
3;
6;
1
2
3
4
5
6
7
8

我们来分析一下整个流程:

Step 1

console.log(1);
1

Stack Queue: [console]

Macrotask Queue: []

Microtask Queue: []

打印结果: 1

Step 2

setTimeout(() => {
  // 这个回调函数叫做callback1,setTimeout属于macrotask,所以放到macrotask queue中
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
});
1
2
3
4
5
6
7

Stack Queue: [setTimeout]

Macrotask Queue: [callback1]

Microtask Queue: []

打印结果: 1

Step 3

new Promise((resolve, reject) => {
  // 注意,这里是同步执行的
  console.log(4);
  resolve(5);
}).then((data) => {
  // 这个回调函数叫做callback2,promise属于microtask,所以放到microtask queue中
  console.log(data);
});
1
2
3
4
5
6
7
8

Stack Queue: [promise]

Macrotask Queue: [callback1] //宏队列

Microtask Queue: [callback2] //微队列

打印结果: 1 4

Step 4

setTimeout(() => {
  // 这个回调函数叫做callback3,setTimeout属于macrotask,所以放到macrotask queue中
  console.log(6);
});
1
2
3
4

Stack Queue: [setTimeout]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

打印结果: 1 4

Step 5 console.log(7)

打印结果: 1 4 7

  • 好啦,全局 Script 代码执行完了,进入下一个步骤,从 microtask queue 中依次取出任务执行,直到 microtask queue 队列为空。

Stack Queue: [console]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

Step 6 console.log(data) // 这里 data 是 Promise 的决议值 5

Stack Queue: [callback2]

Macrotask Queue: [callback1, callback3]

Microtask Queue: []

打印结果:
1
4
7
5
1
2
3
4
5
  • 这里 microtask queue 中只有一个任务,执行完后开始从宏任务队列 macrotask queue 中取位于队首的任务执行

Step 7

console.log(2);
1

Stack Queue: [callback1]

Macrotask Queue: [callback3]

Microtask Queue: []

  • 但是,执行 callback1 的时候又遇到了另一个 Promise,Promise 异步执行完后在 microtask queue 中又注册了一个 callback4 回调函数 Step 8
Promise.resolve().then(() => {
  // 这个回调函数叫做callback4,promise属于microtask,所以放到microtask queue中
  console.log(3);
});
1
2
3
4

Stack Queue: [promise]

Macrotask v: [callback3]

Microtask Queue: [callback4]

  • 取出一个宏任务 macrotask 执行完毕,然后再去微任务队列 microtask queue 中依次取出执行

Step 9

console.log(3);
1

Stack Queue: [callback4]

Macrotask Queue: [callback3]

Microtask Queue: []

  • 微任务队列全部执行完,再去宏任务队列中取第一个任务执行

  • 以上,全部执行完后,Stack Queue 为空,Macrotask Queue 为空,Micro Queue 为空

再来一个例子:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3);
  });
});

new Promise((resolve, reject) => {
  console.log(4);
  resolve(5);
}).then((data) => {
  console.log(data);

  Promise.resolve()
    .then(() => {
      console.log(6);
    })
    .then(() => {
      console.log(7);

      setTimeout(() => {
        console.log(8);
      }, 0);
    });
});

setTimeout(() => {
  console.log(9);
});

console.log(10);
// 正确答案
1;
4;
10;
5;
6;
7;
2;
3;
9;
8;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

在执行微队列 microtask queue 中任务的时候,如果又产生了 microtask,那么会继续添加到队列的末尾,也会在这个周期执行,直到 microtask queue 为空停止。

注:当然如果你在 microtask 中不断的产生 microtask,那么其他宏任务 macrotask 就无法执行了,但是这个操作也不是无限的,拿 NodeJS 中的微任务 process.nextTick()来说,它的上限是 1000 个

# 进阶题目:

//2 3 5 6 4 7 8 1
setTimeout(() => {
  console.log(1);
}, 0);
console.log(2);
async1();

async function async1() {
  console.log(3);
  await async2();
  console.log(4);
  //对await进行改写
  // new Promise((resolve)=>{
  //     async2()
  //     resolve()
  // }).then(()=>{
  //     console.log(4);
  // })
}
requestAnimationFrame(() => {
  console.log(8);
});
async function async2() {
  console.log(5);
}
new Promise((resolve, reject) => {
  console.log(6);
  resolve();
}).then(() => {
  console.log(7);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}

async function async2() {
  console.log("async2 start");
  return new Promise((resolve, reject) => {
    resolve();
    console.log("async2 promise");
  });
}

console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

async1();

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
})
  .then(function () {
    console.log("promise2");
  })
  .then(function () {
    console.log("promise3");
  });

console.log("script end");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
new Promise((resolve, reject) => {
  console.log("async1 start");
  console.log("async2");
  resolve(Promise.resolve());
}).then(() => {
  console.log("async1 end");
});

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
})
  .then(function () {
    console.log("promise2");
  })
  .then(function () {
    console.log("promise3");
  })
  .then(function () {
    console.log("promise4");
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Last Updated: 6/3/2024, 1:08:34 AM